package org.persistente.manipula.mapeamento;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.persistente.manipula.conexao.ConexaoBD;


/**
 * Manipulador de entidades/objetos no banco de dados.
 * @param <T> Tipo de entidade manipulada.
 */
public class Manipulador<T> {


    /**
     * Mapa objeto relacional da entidade atual.
     */
    private MapaObjetoRelacional<T> mapa;

    /**
     * Conexão usada para realizar operações com o banco de dados.
     */
    private Connection conexao;

    public Manipulador(MapaObjetoRelacional<T> mapa, Connection con) {
        this.mapa = mapa;
        this.conexao = con;
    }

    public Manipulador(MapaObjetoRelacional<T> mapa) {
        this(mapa, null);
    }


    public MapaObjetoRelacional<T> getMapaObjetoRelacional() {
        return mapa;
    }
    
    

    public List<CampoAbstrato> getCampos() {
        return mapa.getCampos();
    }

    public List<CampoAbstrato> getChaves() {
        return mapa.getChaves();
    }

    public List<CampoAbstrato> getNaoChaves() {
        return mapa.getNaoChaves();
    }

    public String getNomeEntidade() {
        return mapa.getNomeEntidade();
    }


    /**
     * Informa os valores das variáveis do PreparedStatement com os valores do objeto.
     * @param st Statemnt
     * @param pos Posição da variável.
     * @param entidade Entidade contendo os valores a serem usados.
     * @param campos Campos a serem utilizados.
     * @return Nova posição de variáveis, uma após a última informada.
     */
    public int setParametros(PreparedStatement st, int pos, T entidade, List<CampoAbstrato> campos) throws SQLException {
        for (CampoAbstrato ac : campos) {
            Object val = ac.invocaGet(entidade);
            ac.set(st, pos, val);
            ++pos;
        }
        return pos;
    }

    /**
     * Informa valores, utilizando buscas não exatas em campos string.
     * @param st Statemnt
     * @param pos Posição da variável.
     * @param entidade Entidade contendo os valores a serem usados.
     * @param campos Campos a serem utilizados.
     * @return Nova posição de variáveis, uma após a última informada.
     */
    private int setParametrosNaoExatos(PreparedStatement st, int pos, T entidade, List<CampoAbstrato> campos) throws SQLException {
        for (CampoAbstrato ac : campos) {
            Object val = ac.invocaGet(entidade);
            if(ac instanceof CampoString){
                val = "%" + val + "%";
            }
            ac.set(st, pos, val);
            ++pos;
        }
        return pos;
    }

    /**
     * Preenche um objeto com os valores de um result set.
     * @param rs Result set contendo os valores resgatados do bando de dados.
     * @param entidade Objeto a ser preenchido.
     * @param campos Campos que serão preenchidos.
     */
    public void preencheEntidade(ResultSet rs, T entidade, List<CampoAbstrato> campos) throws SQLException {
        for (CampoAbstrato ac : campos) {
            Object val = ac.get(rs);
            ac.invocaSet(entidade, val);
        }
    }

    /**
     * Retorna um array de campos a partir de um array om o nome dos atributos.
     * @param atributos Array de nomes dos atributos.
     * @return Lista de campos.
     */
    private List<CampoAbstrato> getCamposOrdenacao(String[] atributos) {
        ArrayList<CampoAbstrato> campos = new ArrayList<CampoAbstrato>();
        for (String at : atributos) {
            campos.add(mapa.getCampo(at));
        }
        return campos;
    }
    
    
    /**
     * Verifica quais campos estão modificados, ou diferentes do padrão, em um objeto.
     * @param entidade Objeto a ser verificado.
     * @return Lista de campos modificados.
     */
    public List<CampoAbstrato> getModificados(T entidade) {
        ArrayList<CampoAbstrato> lista = new ArrayList<CampoAbstrato>();
        for (CampoAbstrato ac : getCampos()) {
            Object val = ac.invocaGet(entidade);
            if (val != null) {
                lista.add(ac);
            }
        }
        return lista;
    }

    /**
     * * Verifica quais campos que não fazem parte da chave primária estão modificados, ou diferentes do padrão, em um objeto.
     * @param entidade Objeto a ser verificado.
     * @return Lista de campos modificados.
     */
    public List<CampoAbstrato> getNaoChaveModificados(T entidade) {
        ArrayList<CampoAbstrato> lista = new ArrayList<CampoAbstrato>();
        for (CampoAbstrato ac : getNaoChaves()) {
            Object val = ac.invocaGet(entidade);
            if (val != null) {
                lista.add(ac);
            }
        }
        return lista;
    }

    /**
     * Verifica se há algum valor modificado em um objeto.
     * @param entidade Objeto a ser verificado.
     * @return true para modificado, false caso contrário.
     */
    public boolean estaModificado(T entidade){
        for(CampoAbstrato ac : getCampos()){
            Object val = ac.invocaGet(entidade);
            if(val != null){
                return true;
            }
        }
        return false;
    }


    /**
     * Altera todos os valores do objeto para o valor padrão, tornando o objeto "não-alterado".
     * @param entidade Objeto a ser resetado.
     */
    public void reseta(T entidade){
        for(CampoAbstrato ac : getCampos()){
            ac.invocaSet(entidade, null);
        }
    }

    public Connection getConexao(){
        if(conexao == null){
            return ConexaoBD.novaConexao();
        }
        return conexao;
    }

    public void fecha(Connection con){
        if(con != conexao){
            ConexaoBD.fecha(con);
        }
    }
    
    /**
     * Insere a entidade na tabela correspondente do banco de dados.
     * @param entidade Objeto a ser inserido.
     */
    public int insere(T entidade) {
        Connection con = getConexao();        
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> modificados = getModificados(entidade);
            String sql = GeradorSql.insere(getNomeEntidade(), modificados);
            
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, modificados);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(st);
            fecha(con);
        }
        
    }

    /**
     * Não implementado nesta classe.
     * @param entidade Entidade a ser inserida
     */
    public void insereRetornando(T entidade) {
        throw new java.lang.UnsupportedOperationException("Metodo nao suportado nesta classe");
    }
    
    /**
     * Atualiza todos campos (inclusive nulos) da entidade no banco de dados.
     * @param entidade Entidade a ser atualizada.
     * @return N�mero de registros atualizados.
     */
    public int atualiza(T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> naoChaves = getNaoChaves();
            List<CampoAbstrato> chaves = getChaves();
            String sql = GeradorSql.atualiza(getNomeEntidade(), naoChaves, chaves);
            st = con.prepareStatement(sql);

            int pos = setParametros(st, 1, entidade, naoChaves);
            setParametros(st, pos, entidade, chaves);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Atualiza os campos n�o-nulos da entidade no banco de dados.
     * @param entidade Entidade a ser atualizada.
     * @return N�mero de registros atualizados.
     */
    public int atualizaPorExemplo(T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> modificados = getNaoChaveModificados(entidade);
            List<CampoAbstrato> chaves = getChaves();
            String sql = GeradorSql.atualiza(getNomeEntidade(), modificados, chaves);

            st = con.prepareStatement(sql);
            int pos = setParametros(st, 1, entidade, modificados);
            setParametros(st, pos, entidade, chaves);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Exclui um registro do banco de dados, filtrando pela chave prim�ria.
     * @param entidade entidade a ser excluida do banco de dados.
     * @return N�mero de registros exclu�dos.
     */
    public int exclui(T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> chaves = getChaves();
            String sql = GeradorSql.exclui(getNomeEntidade(), chaves);
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, chaves);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Exclui registros do banco de dados, filtrando pelos dados preenchidos em entidade.
     * @param entidade Objeto que cont�m os dados para o filtro da exclus�o.
     * @return N�mero de registros exclu�dos.
     */
    public int excluiPorExemplo(T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> modificados = getModificados(entidade);
            String sql = GeradorSql.exclui(getNomeEntidade(), modificados);
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, modificados);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Busca um registro no banco de dados pela chave prim�ria.
     * @param entidade Objeto contendo os dados da chave prim�ria.
     * @return Novo objeto correspondente ao registro encontrado.
     */
    public T busca(T entidade) {
        if(entidade == null){
            return null;
        }
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> chaves = getChaves();
            List<CampoAbstrato> campos = getCampos();
            String sql = GeradorSql.pesquisa(getNomeEntidade(), campos, chaves);
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, chaves);
            rs = st.executeQuery();
            if (!rs.next()) {
                return null;
            }
            T nova = (T) entidade.getClass().newInstance();
            preencheEntidade(rs, nova, campos);
            return nova;
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } catch (Exception e) {
            throw new ManipulaException(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Busca um registro no banco de dados pelo exemplo.
     * @param entidade Objeto contendo os dados para filtro.
     * @return Novo objeto correspondente ao registro encontrado ou null se não for encontrado nenhum.
     */
    public T buscaPorExemplo(T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> modificados = getModificados(entidade);
            List<CampoAbstrato> campos = getCampos();
            String sql = GeradorSql.pesquisa(getNomeEntidade(), campos, modificados);
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, modificados);
            rs = st.executeQuery();
            if (!rs.next()) {
                return null;
            }
            T nova = (T) entidade.getClass().newInstance();
            preencheEntidade(rs, nova, campos);
            return nova;
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } catch (Exception e) {
            throw new ManipulaException(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Busca um registro no banco de dados pela chave primária e trava para atualização.
     * @param entidade Objeto contendo os dados da chave prim�ria.
     * @return Novo objeto correspondente ao registro encontrado.
     */
    public T buscaParaAtualizar(T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> chaves = getChaves();
            List<CampoAbstrato> campos = getCampos();
            String sql = GeradorSql.pesquisa(getNomeEntidade(), campos, chaves) + " FOR UPDATE";
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, chaves);
            rs = st.executeQuery();
            if (!rs.next()) {
                return null;
            }
            T nova = (T) entidade.getClass().newInstance();
            preencheEntidade(rs, nova, campos);
            return nova;
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } catch (Exception e) {
            throw new ManipulaException(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }

    /**
     * Pesquisa registros no banco de dados, filtrando pelos dados informados em entidade.
     * @param entidade Objeto contendo os dados para o filtro da pesquisa.
     * @return Lista de objetos correspondentes aos registros resultantes da pesquisa.
     */
    public List<T> pesquisaPorExemplo(T entidade) {
        return pesquisaPorExemplo(entidade, true);
    }

    /**
     * Pesquisa registros no banco de dados, filtrando pelos dados informados em entidade.
     * @param entidade Objeto contendo os dados para o filtro da pesquisa.
     * @param exata Indica se a busca utilizar� busca por express�o regular em campos texto.
     * @return Lista de objetos correspondentes aos registros resultantes da pesquisa.
     */
    public List<T> pesquisaPorExemplo(T entidade, boolean exata) {
        return pesquisaPorExemplo(entidade, exata, new String[0], true);
    }

    /**
     * Pesquisa registros no banco de dados, filtrando pelos dados informados em entidade.
     * @param entidade Objeto contendo os dados para o filtro da pesquisa.
     * @param exata Indica se a busca utilizar� busca por express�o regular em campos texto.
     * @param ordem Atributos pelos quais o resultado da pesquisa ser� ordenado.
     * @param crescente true para ordena��o crescente, false para decrescente.
     * @return Lista de objetos correspondentes aos registros resultantes da pesquisa.
     */
    public List<T> pesquisaPorExemplo(T entidade, boolean exata, String[] ordem, boolean crescente) {
        return pesquisaPorExemplo(entidade, exata, ordem, crescente, "");
    }

    /**
     * Pesquisa registros no banco de dados, filtrando pelos dados informados em entidade.
     * @param entidade Objeto contendo os dados para o filtro da pesquisa.
     * @param exata Indica se a busca utilizar� busca por express�o regular em campos texto.
     * @param ordem Atributos pelos quais o resultado da pesquisa ser� ordenado.
     * @param crescente true para ordena��o crescente, false para decrescente.
     * @param extra Comandos para serem inclu�dos no sql a ser executado. 
     * @return Lista de objetos correspondentes aos registros resultantes da pesquisa.
     */
    public List<T> pesquisaPorExemplo(T entidade, boolean exata, String[] ordem, boolean crescente, String extra) {
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        String sql = "";
        try {
            List<CampoAbstrato> campos = getCampos();
            List<CampoAbstrato> modificados = getModificados(entidade);
            sql = GeradorSql.pesquisa(getNomeEntidade(), campos, modificados, exata, getCamposOrdenacao(ordem), crescente, extra);
            st = con.prepareStatement(sql);
            if(exata){
                setParametros(st, 1, entidade, modificados);
            }
            else{
                setParametrosNaoExatos(st, 1, entidade, modificados);
            }
            rs = st.executeQuery();
            ArrayList<T> lista = new ArrayList<T>();
            while (rs.next()) {
                T nova = (T) entidade.getClass().newInstance();
                preencheEntidade(rs, nova, campos);
                lista.add(nova);
            }
            return lista;
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } catch (Exception e) {
            throw new ManipulaException(e);
        } finally {
            ConexaoBD.fecha(st);
            ConexaoBD.fecha(rs);
            fecha(con);
        }
    }


    /**
     * Retorna a quantidade de registros encontrado na pesquisa pelos campos informados em entidade.
     * @param entidade
     * @return
     */
    public int contaPorExemplo(T entidade){
        return contaPorExemplo(entidade, true);
    }

    /**
     * Retorna a quantidade de registros encontrado na pesquisa pelos campos informados em entidade.
     * @param entidade
     * @param exata
     * @return
     */
    public int contaPorExemplo(T entidade, boolean exata) {
        return contaPorExemplo(entidade, exata, "");
    }

    /**
     * Retorna a quantidade de registros encontrado na pesquisa pelos campos informados em entidade.
     * @param entidade Objeto contendo os dados para o filtro da pesquisa.
     * @param exata Indica se a busca n�o utilizar� busca por express�o regular em campos texto.
     * @return Quantidade de registros encontrados na pesquisa.
     */
    public int contaPorExemplo(T entidade, boolean exata, String extra) {
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> modificados = getModificados(entidade);
            String sql = GeradorSql.seleciona(getNomeEntidade(), "COUNT(*)", modificados, exata);
            st = con.prepareStatement(sql + " " + extra);
          
            setParametros(st, 1, entidade, modificados);
            rs = st.executeQuery();
            if (!rs.next()) {
                throw new ManipulaException("Falha na comunicação com o banco de dados");
            }
            return rs.getInt(1);
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }
    
    
    ///SQLs perigosos:
    
    /**
     * Executa um comando sql select com filtro espec�fico.
     * @param filtroSql - Filtro do comando select que seleciona todos os campos mapeados
     * @param entidade Entidade com dados para o filtro, apenas os dados necessários para o filtro do sql devem estar alterados
     * @return Lista de entidades, resultante do comando select
     */
    public List<T> pesquisaEspecifica(String filtroSql, T entidade){
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> campos = getCampos();
            List<CampoAbstrato> modificados = getModificados(entidade);
            
            String sql = GeradorSql.seleciona(getNomeEntidade(), campos, filtroSql);
            
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, modificados);
            rs = st.executeQuery();
            
            ArrayList<T> lista = new ArrayList<T>();
            while (rs.next()) {
                T nova = (T) entidade.getClass().newInstance();
                preencheEntidade(rs, nova, campos);
                lista.add(nova);
            }
            return lista;
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } catch (Exception e) {
            throw new ManipulaException(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }
    
    /**
     * Seleciona uma coluna do banco de dados, correspondente ao atributo especificado.
     * @param atributo Atributo correspondente � coluna a ser retornada.
     * @param entidade Objeto contendo os dados para o filtro da pesquisa.
     * @return Os elementos da coluna como uma lista de objetos.
     */
    public List<Object> selecionaColuna(String atributo, T entidade){
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> modificados = getModificados(entidade);
            
            CampoAbstrato campo = mapa.getCampo(atributo);
            if(campo == null){
                throw new ManipulaException("Atributo inv�lido!");
            }            
            String retorno = campo.getNome();
            
            String sql = GeradorSql.seleciona(getNomeEntidade(), retorno, modificados, true);
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, modificados);
            rs = st.executeQuery();
            ArrayList<Object> lista = new ArrayList<Object>();
            while(rs.next()){
                lista.add(campo.get(rs));
            }
            return lista;
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }
    
    /**
     * Executa um comando sql select com retorno num�rico especificado.
     * @param retorno Opera��o que retorne um n�mero do banco de dados.
     * @param entidade Objeto contendo os dados para o filtro da opera��o.
     * @return N�mero resultado da opera��o realizada no banco de dados.
     */
    public double calculoEspecifico(String retorno, T entidade) {
        Connection con = getConexao();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            List<CampoAbstrato> modificados = getModificados(entidade);
            
            String sql = GeradorSql.seleciona(getNomeEntidade(), retorno, modificados, true);
            st = con.prepareStatement(sql);
            setParametros(st, 1, entidade, modificados);
            rs = st.executeQuery();
            if (!rs.next()) {
                throw new ManipulaException("Falha na comunica��o com o banco de dados");
            }
            return rs.getDouble(1);
        } catch (SQLException e) {
            throw ConexaoBD.novaExcecao(e);
        } finally {
            ConexaoBD.fecha(rs);
            ConexaoBD.fecha(st);
            fecha(con);
        }
    }    
    
}
